package com.zhouyx.redisactive.lock;

import org.springframework.beans.factory.annotation.Autowired;

import com.zhouyx.redisactive.utils.JedisUtils;

/**
 * @ClassName RedisLock
 * @Description Redis分布式锁
 * @Author za-zhouyunxing
 * @Date 2019/4/16 15:45
 * @Version 1.0
 */
public class RedisLock {
    @Autowired
    private JedisUtils jedis;

    /**
     * 获取锁
     * 问题：如果有多个客户端竞争同一个分布式锁，如果三个客户端中，
     * 有任意一个线程在调用acquire成功之后异常退出，没有释放锁，
     * 另外两个客户端会死循环等待在SETNX命令上。
     * @param lock
     */
    public void acquireV1(String lock){
        while(jedis.setnx(lock, "")){}
    }

    /**
     * 获取锁
     * 这样就解决了死锁的问题
     * 问题：C0操作超时了，但它还持有着锁，C1和C2读取lock.foo检查时间戳，先后发现超时了。
     * C1 发送DEL lock.foo
     * C1 发送SETNX lock.foo 并且成功了。
     * C2 发送DEL lock.foo
     * C2 发送SETNX lock.foo 并且成功了。
     * 这样一来，C1，C2都拿到了锁！问题大了！
     * @param lock
     * @param hold_time
     */
    public void acquireV2(String lock,  Long hold_time){
        //1.先尝试用setnx命令获取锁,key=lock,value=当前时间+要持有锁的时间hold_time
        while(jedis.setnx(lock, String.valueOf(System.currentTimeMillis() + hold_time))){
            //2.如果获取失败,检查lock对应的值是否已超时
            String expireTime = jedis.get(lock);
            if(expireTime != null && Long.parseLong(expireTime) < System.currentTimeMillis()){
                //3.如果已经超时了,删除lock，获取锁
                jedis.del(lock);
                jedis.setnx(lock, String.valueOf(System.currentTimeMillis() + hold_time));
                break;
            }
        }
    }

    /**
     * 获取锁
     * 这样就解决了上述的V1,V2的问题，但这个版本依旧有两个问题没有解决：
     *
     * 1.有效期时间戳覆盖问题：持有锁的客户端1异常退出，其余多个客户端同时执行setnx失败，获取expireTime，
     * 发现已经小于currentTime，开始执行getset命令。假设客户端2先执行了getset，获取锁成功。
     * 客户端3在执行getset时，返回的是客户端2设置的未超时的时间戳，是一个未超时的时间，获取锁失败。
     * 看起来没有问题，但客户端2持有的锁的有效期时间戳已经被客户端3修改了。
     *
     * 2.超时问题：如果客户端2在持有锁的期间，由于操作还没有完成，但锁已经超时了。
     * 这时其它客户端会拿到锁，和超时的客户端一起访问redis，不满足互斥条件。
     * @param lock
     * @param hold_time
     */
    public void acquireV3(String lock,  Long hold_time){
        //1.先尝试用setnx命令获取锁,key=lock,value=当前时间+要持有锁的时间hold_time
        while(jedis.setnx(lock, String.valueOf(System.currentTimeMillis() + hold_time))){
            //2.如果获取失败,检查lock对应的值是否已超时
            String expireTime = jedis.get(lock);
            if(expireTime != null && Long.parseLong(expireTime) < System.currentTimeMillis()){
                //3.如果已经超时了,使用getset命令,设置新的超时时间
                String oldExpire = jedis.getSet(lock, String.valueOf(System.currentTimeMillis() + hold_time));
                if(oldExpire != null && Long.parseLong(expireTime) < System.currentTimeMillis()){
                    //4.如果setget命令返回的值,依然是过期时间,认为获取锁成功
                    break;
                }
            }
        }
    }

    /**
     * 释放锁
     * @param lock
     */
    public void release(String lock){
        jedis.del(lock);
    }
}
